# -*- coding: utf-8 -*-
"""
This module implements the class that deals with the full document.

..  :copyright: (c) 2014 by Jelte Fennema.
    :license: MIT, see License for more details.
"""

import errno
import os
import subprocess
import sys

import pylatex.config as cf

from .base_classes import (
    Command,
    Container,
    Environment,
    LatexObject,
    SpecialArguments,
    UnsafeCommand,
)
from .errors import CompilerError
from .package import Package
from .utils import NoEscape, dumps_list, rm_temp_dir


class Document(Environment):
    r"""
    A class that contains a full LaTeX document.

    If needed, you can append stuff to the preamble or the packages.
    For instance, if you need to use ``\maketitle`` you can add the title,
    author and date commands to the preamble to make it work.

    Example
    -------
    >>> import pylatex
    >>> import pathlib
    >>> import tempfile
    >>> # Create a place where we can write our PDF to disk
    >>> temp_output_path = pathlib.Path(tempfile.mkdtemp())
    >>> temp_output_path.mkdir(exist_ok=True)
    >>> document_fpath = temp_output_path / 'my_document.pdf'
    >>> # The Document class is the main point of interaction.
    >>> doc = pylatex.Document(
    >>>     document_fpath.with_suffix(''),  # give the output file path without the .pdf
    >>>     inputenc=None,
    >>>     page_numbers=False,
    >>>     indent=False,
    >>>     fontenc=None,
    >>>     lmodern=True,
    >>>     textcomp=False,
    >>>     documentclass='article',
    >>>     geometry_options='paperheight=0.4in,paperwidth=1in,margin=0.1in',
    >>> )
    >>> # Append content to the document, which can be plain text, or
    >>> # object from pylatex. For now lets just say hello!
    >>> doc.append('Hello World')
    >>> # Inspect the generated latex
    >>> print(doc.dumps())
    \documentclass{article}%
    \usepackage{lmodern}%
    \usepackage{parskip}%
    \usepackage{geometry}%
    \geometry{paperheight=0.4in,paperwidth=1in,margin=0.1in}%
    %
    %
    %
    \begin{document}%
    \pagestyle{empty}%
    \normalsize%
    Hello World%
    \end{document}
    >>> # Generate and the PDF in document_fpath
    >>> doc.generate_pdf()
    """

    def __init__(
        self,
        default_filepath="default_filepath",
        *,
        documentclass="article",
        document_options=None,
        fontenc="T1",
        inputenc="utf8",
        font_size="normalsize",
        lmodern=True,
        textcomp=True,
        microtype=None,
        page_numbers=True,
        indent=None,
        geometry_options=None,
        data=None
    ):
        r"""
        Args
        ----
        default_filepath: str
            The default path to save files.
        documentclass: str or `~.Command`
            The LaTeX class of the document.
        document_options: str or `list`
            The options to supply to the documentclass
        fontenc: str
            The option for the fontenc package. If it is `None`, the fontenc
            package will not be loaded at all.
        inputenc: str
            The option for the inputenc package. If it is `None`, the inputenc
            package will not be loaded at all.
        font_size: str
            The font size to declare as normalsize
        lmodern: bool
            Use the Latin Modern font. This is a font that contains more glyphs
            than the standard LaTeX font.
        textcomp: bool
            Adds even more glyphs, for instance the Euro (€) sign.
        page_numbers: bool
            Adds the ability to add the last page to the document.
        indent: bool
            Determines whether or not the document requires indentation. If it
            is `None` it will use the value from the active config. Which is
            `True` by default.
        geometry_options: dict
            The options to supply to the geometry package
        data: list
            Initial content of the document.
        """

        self.default_filepath = default_filepath

        if isinstance(documentclass, Command):
            self.documentclass = documentclass
        else:
            self.documentclass = Command(
                "documentclass", arguments=documentclass, options=document_options
            )
        if indent is None:
            indent = cf.active.indent
        if microtype is None:
            microtype = cf.active.microtype

        # These variables are used by the __repr__ method
        self._fontenc = fontenc
        self._inputenc = inputenc
        self._lmodern = lmodern
        self._indent = indent
        self._microtype = microtype

        packages = []

        if fontenc is not None:
            packages.append(Package("fontenc", options=fontenc))
        if inputenc is not None:
            packages.append(Package("inputenc", options=inputenc))
        if lmodern:
            packages.append(Package("lmodern"))
        if textcomp:
            packages.append(Package("textcomp"))
        if page_numbers:
            packages.append(Package("lastpage"))
        if not indent:
            packages.append(Package("parskip"))
        if microtype:
            packages.append(Package("microtype"))

        if geometry_options is not None:
            packages.append(Package("geometry"))
            # Make sure we don't add this options command for an empty list,
            # because that breaks.
            if geometry_options:
                packages.append(
                    Command(
                        "geometry",
                        arguments=SpecialArguments(geometry_options),
                    )
                )

        super().__init__(data=data)

        # Usually the name is the class name, but if we create our own
        # document class, \begin{document} gets messed up.
        self._latex_name = "document"

        self.packages |= packages
        self.variables = []

        self.preamble = []

        if not page_numbers:
            self.change_document_style("empty")

        # No colors have been added to the document yet
        self.color = False
        self.meta_data = False

        self.append(Command(command=font_size))

    def _propagate_packages(self):
        r"""Propogate packages.

        Make sure that all the packages included in the previous containers
        are part of the full list of packages.
        """

        super()._propagate_packages()

        for item in self.preamble:
            if isinstance(item, LatexObject):
                if isinstance(item, Container):
                    item._propagate_packages()
                for p in item.packages:
                    self.packages.add(p)

    def dumps(self):
        """Represent the document as a string in LaTeX syntax.

        Returns
        -------
        str
        """

        head = self.documentclass.dumps() + "%\n"
        head += self.dumps_packages() + "%\n"
        head += dumps_list(self.variables) + "%\n"
        head += dumps_list(self.preamble) + "%\n"

        return head + "%\n" + super().dumps()

    def generate_tex(self, filepath=None):
        """Generate a .tex file for the document.

        Args
        ----
        filepath: str
            The name of the file (without .tex), if this is not supplied the
            default filepath attribute is used as the path.
        """

        super().generate_tex(self._select_filepath(filepath))

    def generate_pdf(
        self,
        filepath=None,
        *,
        clean=True,
        clean_tex=True,
        compiler=None,
        compiler_args=None,
        silent=True
    ):
        """Generate a pdf file from the document.

        Args
        ----
        filepath: str
            The name of the file (without .pdf), if it is `None` the
            ``default_filepath`` attribute will be used.
        clean: bool
            Whether non-pdf files created that are created during compilation
            should be removed.
        clean_tex: bool
            Also remove the generated tex file.
        compiler: `str` or `None`
            The name of the LaTeX compiler to use. If it is None, PyLaTeX will
            choose a fitting one on its own. Starting with ``latexmk`` and then
            ``pdflatex``.
        compiler_args: `list` or `None`
            Extra arguments that should be passed to the LaTeX compiler. If
            this is None it defaults to an empty list.
        silent: bool
            Whether to hide compiler output
        """

        if compiler_args is None:
            compiler_args = []

        # In case of newer python with the use of the cwd parameter
        # one can avoid to physically change the directory
        # to the destination folder
        python_cwd_available = sys.version_info >= (3, 6)

        filepath = self._select_filepath(filepath)
        if not os.path.basename(filepath):
            filepath = os.path.join(os.path.abspath(filepath), "default_basename")
        else:
            filepath = os.path.abspath(filepath)

        cur_dir = os.getcwd()
        dest_dir = os.path.dirname(filepath)

        if not python_cwd_available:
            os.chdir(dest_dir)

        self.generate_tex(filepath)

        if compiler is not None:
            compilers = ((compiler, []),)
        else:
            latexmk_args = ["--pdf"]

            compilers = (("latexmk", latexmk_args), ("pdflatex", []))

        main_arguments = ["--interaction=nonstopmode", filepath + ".tex"]

        check_output_kwargs = {}
        if python_cwd_available:
            check_output_kwargs = {"cwd": dest_dir}

        os_error = None

        for compiler, arguments in compilers:
            command = [compiler] + arguments + compiler_args + main_arguments

            try:
                output = subprocess.check_output(
                    command, stderr=subprocess.STDOUT, **check_output_kwargs
                )
            except (OSError, IOError) as e:
                # Use FileNotFoundError when python 2 is dropped
                os_error = e

                if os_error.errno == errno.ENOENT:
                    # If compiler does not exist, try next in the list
                    continue
                raise
            except subprocess.CalledProcessError as e:
                # For all other errors print the output and raise the error
                print(e.output.decode())
                raise
            else:
                if not silent:
                    print(output.decode())

            if clean:
                try:
                    # Try latexmk cleaning first
                    subprocess.check_output(
                        ["latexmk", "-c", filepath],
                        stderr=subprocess.STDOUT,
                        **check_output_kwargs
                    )
                except (OSError, IOError, subprocess.CalledProcessError):
                    # Otherwise just remove some file extensions.
                    extensions = ["aux", "log", "out", "fls", "fdb_latexmk"]

                    for ext in extensions:
                        try:
                            os.remove(filepath + "." + ext)
                        except (OSError, IOError) as e:
                            # Use FileNotFoundError when python 2 is dropped
                            if e.errno != errno.ENOENT:
                                raise
                rm_temp_dir()

            if clean_tex:
                os.remove(filepath + ".tex")  # Remove generated tex file

            # Compilation has finished, so no further compilers have to be
            # tried
            break

        else:
            # Notify user that none of the compilers worked.
            raise (
                CompilerError(
                    "No LaTex compiler was found\n"
                    "Either specify a LaTex compiler "
                    "or make sure you have latexmk or pdfLaTex installed."
                )
            )

        if not python_cwd_available:
            os.chdir(cur_dir)

    def _select_filepath(self, filepath):
        """Make a choice between ``filepath`` and ``self.default_filepath``.

        Args
        ----
        filepath: str
            the filepath to be compared with ``self.default_filepath``

        Returns
        -------
        str
            The selected filepath
        """

        if filepath is None:
            return self.default_filepath
        else:
            if os.path.basename(filepath) == "":
                filepath = os.path.join(
                    filepath, os.path.basename(self.default_filepath)
                )
            return filepath

    def change_page_style(self, style):
        r"""Alternate page styles of the current page.

        Args
        ----
        style: str
            value to set for the page style of the current page
        """

        self.append(Command("thispagestyle", arguments=style))

    def change_document_style(self, style):
        r"""Alternate page style for the entire document.

        Args
        ----
        style: str
            value to set for the document style
        """

        self.append(Command("pagestyle", arguments=style))

    def add_color(self, name, model, description):
        r"""Add a color that can be used throughout the document.

        Args
        ----
        name: str
            Name to set for the color
        model: str
            The color model to use when defining the color
        description: str
            The values to use to define the color
        """

        if self.color is False:
            self.packages.append(Package("color"))
            self.color = True

        self.preamble.append(
            Command("definecolor", arguments=[name, model, description])
        )

    def change_length(self, parameter, value):
        r"""Change the length of a certain parameter to a certain value.

        Args
        ----
        parameter: str
            The name of the parameter to change the length for
        value: str
            The value to set the parameter to
        """

        self.preamble.append(UnsafeCommand("setlength", arguments=[parameter, value]))

    def set_variable(self, name, value):
        r"""Add a variable which can be used inside the document.

        Variables are defined before the preamble. If a variable with that name
        has already been set, the new value will override it for future uses.
        This is done by appending ``\renewcommand`` to the document.

        Args
        ----
        name: str
            The name to set for the variable
        value: str
            The value to set for the variable
        """

        name_arg = "\\" + name
        variable_exists = False

        for variable in self.variables:
            if name_arg == variable.arguments._positional_args[0]:
                variable_exists = True
                break

        if variable_exists:
            renew = Command(
                command="renewcommand", arguments=[NoEscape(name_arg), value]
            )
            self.append(renew)
        else:
            new = Command(command="newcommand", arguments=[NoEscape(name_arg), value])
            self.variables.append(new)
